.. _Tutorial: using NeurEco Python API for a Tabular Regression problem:

Tutorial: using NeurEco Python API for a Tabular Regression problem
=======================================================================

The following section uses the test case :std:ref:`Metamaterial Antennas test case`. This test case is included in the NeurEco installation package.

Build a model
---------------

* Create an empty directory (MetamaterialAntenna Example), extract the :std:ref:`Metamaterial Antennas test case` test case data there. The created directory contains the following files:

  * x_test.csv
  * y_test.csv
  * x_train.csv
  * y_train.csv

* Import the required libraries (NeurEco and NumPy):

.. code-block:: python

  from NeurEco import NeurEcoTabular as Tabular
  import numpy as np

* Load the training data:

.. code-block:: python

  x_train = np.genfromtxt("x_train.csv", delimiter=";", skip_header=True)
  y_train = np.genfromtxt("y_train.csv", delimiter=";", skip_header=True)

* Initialize a NeurEco object to handle the **Regression** problem:

.. code-block:: python

  builder = Tabular.Regressor()

All the methods provided by the **Regressor** class can be viewed by calling the *__methods__* attributes:

.. code-block:: python

  print(builder.__methods__)

.. code-block:: text
    
  *** NeurEco Tabular Regressor methods: ***
  - load
  - save
  - delete
  - evaluate
  - build
  - get_input_count
  - get_output_count
  - load_model_from_checkpoint
  - get_number_of_networks_from_checkpoint
  - get_weights
  - export_fmu
  - export_c
  - export_onnx
  - export_vba
  - compute_error
  - plot_network
  - forward_derivative
  - gradient
  - set_weights
  - perform_input_sweep

To understand what each parameter of any method does and how to use it, print the doc of the method:

.. code-block:: python

  print(builder.export_c.__doc__)

.. code-block:: text

    exports a NeurEco tabular model to a header file
    :param h_file_path: path where the .h file will be saved
    :param precision: string: optional: "float" or "double": precision of the weights in the h file
    :return: export_status: int: 0 if export is ok, other if otherwise.

* To build the model, run the **build** method with the building parameters adjusted to the problem at hand (see :std:ref:`Build NeurEco Regression model with the Python API`). For this example, the outputs are normalized per feature (meaning that each output is normalized apart, see :std:ref:`Normalizing the data`):

.. code-block:: python

  builder.build(input_data=x_train, output_data=y_train,
            # the rest of these parameters are optional
            write_model_to="./MetamaterialAntennas/MetamaterialAntennas",
            checkpoint_address="./MetamaterialAntennas/MetamaterialAntennas.checkpoint",
            outputs_normalize_per_feature=True)

* When **build** is called, NeurEco starts the building process:

.. code-block:: text

  Validation Percentage will be used to get the validation data. This is due to:
  - one or all the validation data is set to None
  - validation indices is set to None

   info >
   info >      _   __                ______
   info >     / | / /__  __  _______/ ____/________
   info >    /  |/ / _ \/ / / / ___/ __/ / ___/ __ \
   info >   / /|  /  __/ /_/ / /  / /___/ /__/ /_/ /
   info >  /_/ |_/\___/\__,_/_/  /_____/\___/\____/
   info >                  === A D A G O S ===
   info >
   info > Version: 4.01.2474.0 Compiled with MSVC v1928  Oct 12 2022 Matlab runtime:no
   info > OpenMP: yes
   info > MKL: yes
   info > Reading data files...
   info > Reading Data from C:/Users/Sadok/AppData/Local/Temp/tmp3wmwwjv4/inputs_tab_train.npy
   info > Reading Data from C:/Users/Sadok/AppData/Local/Temp/tmp3wmwwjv4/outputs_tab_train.npy
   info > build for: 6 outputs and 7 inputs and 457 samples.
   info > Building Model

During the build NeurEco saves the intermediate modes to the checkpoint file (defined by the parameter **checkpoint_address**). 
To load and use the intermediate models from this checkpoint: 

* Create a new NeurEco object in which to load the model:

.. code-block:: python

  model = Tabular.Regressor()
 
* Determine how many intermediate models the checkpoint contains:

.. code-block:: python

  n = model.get_number_of_networks_from_checkpoint("./MetamaterialAntennas/MetamaterialAntennas.checkpoint")

* Load any intermediate model from the checkpoint using its id (count starts with zero). For this example, at the moment of running the command :math:`n=6`, and the following command loads the intermediate model :math:`n°3 \ (id=2)`:

.. code-block:: python

  model.load_model_from_checkpoint("./MetamaterialAntennas/MetamaterialAntennas.checkpoint", 2)

Now **model** is a valid **Regression** model, and can be used as usual. 

* Check the number of trainable parameters each of the intermediate models has:

.. code-block:: python

  for i in range(n):
      print("Loading model", i, " from checkpoint file:")
      model.load_model_from_checkpoint("./MetamaterialAntennas/MetamaterialAntennas.checkpoint", i)
      print("number of trainable parameters in intermediate model --", i, " is:", model.get_weights().size)

.. code-block:: text

  Loading model 0  from checkpoint file:
  number of trainable parameters in intermediate model -- 0  is: 34
  Loading model 1  from checkpoint file:
  number of trainable parameters in intermediate model -- 1  is: 62
  Loading model 2  from checkpoint file:
  number of trainable parameters in intermediate model -- 2  is: 90
  Loading model 3  from checkpoint file:
  number of trainable parameters in intermediate model -- 3  is: 132


Evaluate a model
------------------

* Load the testing data from the CSV files:

.. code-block:: python

  x_test = np.genfromtxt("x_test.csv", delimiter=";", skip_header=True)
  y_test = np.genfromtxt("y_test.csv", delimiter=";", skip_header=True)

* Create a **Regressor** object to use for the evaluation:

.. code-block:: python

  evaluator = Tabular.Regressor()

.. note::
    It is possible to use the already existing **Regressor** object **builder** when the evaluation is done just after the **build**, and **builder** is still available.

* Load the built model:

.. code-block:: python

  load_state = evaluator.load("./MetamaterialAntennas/MetamaterialAntennas")

.. note::
    When building or evaluating a NeurEco model, all the used paths do not necessarily need to have an extension when they are passed as parameters to a NeurEco method.

* To extract information from the loaded model, such as the number of inputs, the number of outputs and the weights array, run:

.. code-block:: python

  n_inputs = evaluator.get_input_count()
  n_outputs = evaluator.get_output_count()
  weights = evaluator.get_weights()
  print("Number of Inputs:", n_inputs)
  print("Number of Outputs:", n_outputs)
  print("Number of trainable parameters:", weights.size)


.. code-block:: text

  Number of Inputs: 7
  Number of Outputs: 6
  Number of trainable parameters: 458

* To plot the network graph (this operation requires *matplotlib* library installed, see :std:ref:`Plot a NeurEco network Regression Python API`):

.. code-block:: python

  evaluator.plot_network()

.. figure:: ./images/MetamaterialAntennaPythonNetworkPlot.png
    :width: 800
    :alt: MetamaterialAntennaPythonNetworkPlot
    :align: center

    Python API operations: plotting a network: test case - MetamaterialAntennas 


* To evaluate the model on the test data:

.. code-block:: python

  neureco_outputs = evaluator.evaluate(x_test)
  l2_error = evaluator.compute_error(neureco_outputs, y_test)
  print("L2 relative error (%):", 100 * l2_error)

.. code-block:: text

  L2 relative error (%): 1.4545507588248332

.. note::
  During evaluation, the normalization is carried out by the model and its parameters are not relative to the data set being evaluated, but are the global parameters computed during the **build** of the model.

* To perform an input sweep (see :std:ref:`Performing an input sweep`, this operation requires *matplotlib* library installed), run, for example:

.. code-block:: python

  evaluator.perform_input_sweep(x=x_test[49, :], input_id=0, input_interval=[2143, 6956.], output_id=0)

.. figure:: ./images/MetamaterialAntennaPythonInputSweep.png
  :width: 800
  :alt: MetamaterialAntennaPythonInputSweep
  :align: center

  Python API operations: Performing an input sweep: test case - MetamaterialAntennas
  
* To save the model in the native NeurEco binary format:

.. code-block:: python

  save_state = evaluator.save("MetamaterialAntennas/NewDir/SameModel")

* To export the model, run one of the following commands (*embed* license is required):

.. code-block:: python

  evaluator.export_c("./MetamaterialAntennas/MetamaterialAntennas.h", precision="float")
  evaluator.export_onnx("./MetamaterialAntennas/MetamaterialAntennas.onnx", precision="float")
  evaluator.export_fmu("./MetamaterialAntennas/MetamaterialAntennas.fmu")
  evaluator.export_vba("./MetamaterialAntennas/MetamaterialAntennas.bas")

.. warning::
  Once the NeurEco object is no longer needed, free the memory by deleting the object by calling the **delete** method. For the example above, three objects must be deleted:

  .. code-block:: python

    builder.delete()
    evaluator.delete()
    model.delete()
